// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/cert/nss_cert_database_chromeos.h"

#include <memory>

#include "base/bind.h"
#include "base/callback.h"
#include "base/run_loop.h"
#include "base/threading/thread_task_runner_handle.h"
#include "crypto/nss_util_internal.h"
#include "crypto/scoped_test_nss_chromeos_user.h"
#include "crypto/scoped_test_nss_db.h"
#include "net/cert/cert_database.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {

namespace {

    bool IsCertInCertificateList(const X509Certificate* cert,
        const CertificateList& cert_list)
    {
        for (CertificateList::const_iterator it = cert_list.begin();
             it != cert_list.end();
             ++it) {
            if (X509Certificate::IsSameOSCert((*it)->os_cert_handle(),
                    cert->os_cert_handle()))
                return true;
        }
        return false;
    }

    void SwapCertLists(CertificateList* destination,
        std::unique_ptr<CertificateList> source)
    {
        ASSERT_TRUE(destination);
        ASSERT_TRUE(source);

        destination->swap(*source);
    }

} // namespace

class NSSCertDatabaseChromeOSTest : public testing::Test,
                                    public CertDatabase::Observer {
public:
    NSSCertDatabaseChromeOSTest()
        : observer_added_(false)
        , user_1_("user1")
        , user_2_("user2")
    {
    }

    void SetUp() override
    {
        // Initialize nss_util slots.
        ASSERT_TRUE(user_1_.constructed_successfully());
        ASSERT_TRUE(user_2_.constructed_successfully());
        user_1_.FinishInit();
        user_2_.FinishInit();

        // Create NSSCertDatabaseChromeOS for each user.
        db_1_.reset(new NSSCertDatabaseChromeOS(
            crypto::GetPublicSlotForChromeOSUser(user_1_.username_hash()),
            crypto::GetPrivateSlotForChromeOSUser(
                user_1_.username_hash(),
                base::Callback<void(crypto::ScopedPK11Slot)>())));
        db_1_->SetSlowTaskRunnerForTest(base::ThreadTaskRunnerHandle::Get());
        db_1_->SetSystemSlot(
            crypto::ScopedPK11Slot(PK11_ReferenceSlot(system_db_.slot())));
        db_2_.reset(new NSSCertDatabaseChromeOS(
            crypto::GetPublicSlotForChromeOSUser(user_2_.username_hash()),
            crypto::GetPrivateSlotForChromeOSUser(
                user_2_.username_hash(),
                base::Callback<void(crypto::ScopedPK11Slot)>())));
        db_2_->SetSlowTaskRunnerForTest(base::ThreadTaskRunnerHandle::Get());

        // Add observer to CertDatabase for checking that notifications from
        // NSSCertDatabaseChromeOS are proxied to the CertDatabase.
        CertDatabase::GetInstance()->AddObserver(this);
        observer_added_ = true;
    }

    void TearDown() override
    {
        if (observer_added_)
            CertDatabase::GetInstance()->RemoveObserver(this);
    }

    // CertDatabase::Observer:
    void OnCertAdded(const X509Certificate* cert) override
    {
        added_.push_back(cert ? cert->os_cert_handle() : NULL);
    }

    void OnCertRemoved(const X509Certificate* cert) override { }

    void OnCACertChanged(const X509Certificate* cert) override
    {
        added_ca_.push_back(cert ? cert->os_cert_handle() : NULL);
    }

protected:
    bool observer_added_;
    // Certificates that were passed to the CertDatabase observers.
    std::vector<CERTCertificate*> added_ca_;
    std::vector<CERTCertificate*> added_;

    crypto::ScopedTestNSSChromeOSUser user_1_;
    crypto::ScopedTestNSSChromeOSUser user_2_;
    crypto::ScopedTestNSSDB system_db_;
    std::unique_ptr<NSSCertDatabaseChromeOS> db_1_;
    std::unique_ptr<NSSCertDatabaseChromeOS> db_2_;
};

// Test that ListModules() on each user includes that user's NSS software slot,
// and does not include the software slot of the other user. (Does not check the
// private slot, since it is the same as the public slot in tests.)
TEST_F(NSSCertDatabaseChromeOSTest, ListModules)
{
    CryptoModuleList modules_1;
    CryptoModuleList modules_2;

    db_1_->ListModules(&modules_1, false /* need_rw */);
    db_2_->ListModules(&modules_2, false /* need_rw */);

    bool found_1 = false;
    for (CryptoModuleList::iterator it = modules_1.begin(); it != modules_1.end();
         ++it) {
        EXPECT_NE(db_2_->GetPublicSlot().get(), (*it)->os_module_handle());
        if ((*it)->os_module_handle() == db_1_->GetPublicSlot().get())
            found_1 = true;
    }
    EXPECT_TRUE(found_1);

    bool found_2 = false;
    for (CryptoModuleList::iterator it = modules_2.begin(); it != modules_2.end();
         ++it) {
        EXPECT_NE(db_1_->GetPublicSlot().get(), (*it)->os_module_handle());
        if ((*it)->os_module_handle() == db_2_->GetPublicSlot().get())
            found_2 = true;
    }
    EXPECT_TRUE(found_2);
}

// Test that ImportCACerts imports the cert to the correct slot, and that
// ListCerts includes the added cert for the correct user, and does not include
// it for the other user.
TEST_F(NSSCertDatabaseChromeOSTest, ImportCACerts)
{
    // Load test certs from disk.
    CertificateList certs_1 = CreateCertificateListFromFile(GetTestCertsDirectory(),
        "root_ca_cert.pem",
        X509Certificate::FORMAT_AUTO);
    ASSERT_EQ(1U, certs_1.size());

    CertificateList certs_2 = CreateCertificateListFromFile(GetTestCertsDirectory(),
        "2048-rsa-root.pem",
        X509Certificate::FORMAT_AUTO);
    ASSERT_EQ(1U, certs_2.size());

    // Import one cert for each user.
    NSSCertDatabase::ImportCertFailureList failed;
    EXPECT_TRUE(
        db_1_->ImportCACerts(certs_1, NSSCertDatabase::TRUSTED_SSL, &failed));
    EXPECT_EQ(0U, failed.size());
    failed.clear();
    EXPECT_TRUE(
        db_2_->ImportCACerts(certs_2, NSSCertDatabase::TRUSTED_SSL, &failed));
    EXPECT_EQ(0U, failed.size());

    // Get cert list for each user.
    CertificateList user_1_certlist;
    CertificateList user_2_certlist;
    db_1_->ListCertsSync(&user_1_certlist);
    db_2_->ListCertsSync(&user_2_certlist);

    // Check that the imported certs only shows up in the list for the user that
    // imported them.
    EXPECT_TRUE(IsCertInCertificateList(certs_1[0].get(), user_1_certlist));
    EXPECT_FALSE(IsCertInCertificateList(certs_1[0].get(), user_2_certlist));

    EXPECT_TRUE(IsCertInCertificateList(certs_2[0].get(), user_2_certlist));
    EXPECT_FALSE(IsCertInCertificateList(certs_2[0].get(), user_1_certlist));

    // Run the message loop so the observer notifications get processed.
    base::RunLoop().RunUntilIdle();
    // Should have gotten two OnCACertChanged notifications.
    ASSERT_EQ(2U, added_ca_.size());
    // TODO(mattm): make NSSCertDatabase actually pass the cert to the callback,
    // and enable these checks:
    // EXPECT_EQ(certs_1[0]->os_cert_handle(), added_ca_[0]);
    // EXPECT_EQ(certs_2[0]->os_cert_handle(), added_ca_[1]);
    EXPECT_EQ(0U, added_.size());

    // Tests that the new certs are loaded by async ListCerts method.
    CertificateList user_1_certlist_async;
    CertificateList user_2_certlist_async;
    db_1_->ListCerts(
        base::Bind(&SwapCertLists, base::Unretained(&user_1_certlist_async)));
    db_2_->ListCerts(
        base::Bind(&SwapCertLists, base::Unretained(&user_2_certlist_async)));

    base::RunLoop().RunUntilIdle();

    EXPECT_TRUE(IsCertInCertificateList(certs_1[0].get(), user_1_certlist_async));
    EXPECT_FALSE(
        IsCertInCertificateList(certs_1[0].get(), user_2_certlist_async));

    EXPECT_TRUE(IsCertInCertificateList(certs_2[0].get(), user_2_certlist_async));
    EXPECT_FALSE(
        IsCertInCertificateList(certs_2[0].get(), user_1_certlist_async));
}

// Test that ImportServerCerts imports the cert to the correct slot, and that
// ListCerts includes the added cert for the correct user, and does not include
// it for the other user.
TEST_F(NSSCertDatabaseChromeOSTest, ImportServerCert)
{
    // Load test certs from disk.
    CertificateList certs_1 = CreateCertificateListFromFile(
        GetTestCertsDirectory(), "ok_cert.pem", X509Certificate::FORMAT_AUTO);
    ASSERT_EQ(1U, certs_1.size());

    CertificateList certs_2 = CreateCertificateListFromFile(GetTestCertsDirectory(),
        "2048-rsa-ee-by-2048-rsa-intermediate.pem",
        X509Certificate::FORMAT_AUTO);
    ASSERT_EQ(1U, certs_2.size());

    // Import one cert for each user.
    NSSCertDatabase::ImportCertFailureList failed;
    EXPECT_TRUE(
        db_1_->ImportServerCert(certs_1, NSSCertDatabase::TRUSTED_SSL, &failed));
    EXPECT_EQ(0U, failed.size());
    failed.clear();
    EXPECT_TRUE(
        db_2_->ImportServerCert(certs_2, NSSCertDatabase::TRUSTED_SSL, &failed));
    EXPECT_EQ(0U, failed.size());

    // Get cert list for each user.
    CertificateList user_1_certlist;
    CertificateList user_2_certlist;
    db_1_->ListCertsSync(&user_1_certlist);
    db_2_->ListCertsSync(&user_2_certlist);

    // Check that the imported certs only shows up in the list for the user that
    // imported them.
    EXPECT_TRUE(IsCertInCertificateList(certs_1[0].get(), user_1_certlist));
    EXPECT_FALSE(IsCertInCertificateList(certs_1[0].get(), user_2_certlist));

    EXPECT_TRUE(IsCertInCertificateList(certs_2[0].get(), user_2_certlist));
    EXPECT_FALSE(IsCertInCertificateList(certs_2[0].get(), user_1_certlist));

    // Run the message loop so the observer notifications get processed.
    base::RunLoop().RunUntilIdle();
    // TODO(mattm): ImportServerCert doesn't actually cause any observers to
    // fire. Is that correct?
    EXPECT_EQ(0U, added_ca_.size());
    EXPECT_EQ(0U, added_.size());

    // Tests that the new certs are loaded by async ListCerts method.
    CertificateList user_1_certlist_async;
    CertificateList user_2_certlist_async;
    db_1_->ListCerts(
        base::Bind(&SwapCertLists, base::Unretained(&user_1_certlist_async)));
    db_2_->ListCerts(
        base::Bind(&SwapCertLists, base::Unretained(&user_2_certlist_async)));

    base::RunLoop().RunUntilIdle();

    EXPECT_TRUE(IsCertInCertificateList(certs_1[0].get(), user_1_certlist_async));
    EXPECT_FALSE(
        IsCertInCertificateList(certs_1[0].get(), user_2_certlist_async));

    EXPECT_TRUE(IsCertInCertificateList(certs_2[0].get(), user_2_certlist_async));
    EXPECT_FALSE(
        IsCertInCertificateList(certs_2[0].get(), user_1_certlist_async));
}

// Tests that There is no crash if the database is deleted while ListCerts
// is being processed on the worker pool.
TEST_F(NSSCertDatabaseChromeOSTest, NoCrashIfShutdownBeforeDoneOnWorkerPool)
{
    CertificateList certlist;
    db_1_->ListCerts(base::Bind(&SwapCertLists, base::Unretained(&certlist)));
    EXPECT_EQ(0U, certlist.size());

    db_1_.reset();

    base::RunLoop().RunUntilIdle();

    EXPECT_LT(0U, certlist.size());
}

TEST_F(NSSCertDatabaseChromeOSTest, ListCertsReadsSystemSlot)
{
    scoped_refptr<X509Certificate> cert_1(
        ImportClientCertAndKeyFromFile(GetTestCertsDirectory(),
            "client_1.pem",
            "client_1.pk8",
            db_1_->GetPublicSlot().get()));

    scoped_refptr<X509Certificate> cert_2(
        ImportClientCertAndKeyFromFile(GetTestCertsDirectory(),
            "client_2.pem",
            "client_2.pk8",
            db_1_->GetSystemSlot().get()));
    CertificateList certs;
    db_1_->ListCertsSync(&certs);
    EXPECT_TRUE(IsCertInCertificateList(cert_1.get(), certs));
    EXPECT_TRUE(IsCertInCertificateList(cert_2.get(), certs));
}

TEST_F(NSSCertDatabaseChromeOSTest, ListCertsDoesNotCrossReadSystemSlot)
{
    scoped_refptr<X509Certificate> cert_1(
        ImportClientCertAndKeyFromFile(GetTestCertsDirectory(),
            "client_1.pem",
            "client_1.pk8",
            db_2_->GetPublicSlot().get()));

    scoped_refptr<X509Certificate> cert_2(
        ImportClientCertAndKeyFromFile(GetTestCertsDirectory(),
            "client_2.pem",
            "client_2.pk8",
            system_db_.slot()));
    CertificateList certs;
    db_2_->ListCertsSync(&certs);
    EXPECT_TRUE(IsCertInCertificateList(cert_1.get(), certs));
    EXPECT_FALSE(IsCertInCertificateList(cert_2.get(), certs));
}

} // namespace net
